package se.webbzon.oschi01.worldeditor;

import java.nio.ByteBuffer;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;

import se.webbzon.oschi01.friendly.Friendly;
import se.webbzon.oschi01.worldarea.WorldArea;
import se.webbzon.oschi01.worldobject.WorldObject;

public class WorldAreaBlueprint {
	
	// The default 3dwa-file version upon creation of a new WorldAreaBlueprint.
	private static final int defaultVersion = 1;

	// The version of the world area blueprint loaded from a 3dwa-file
	private final int version;
	
	// The name of the context in which this world area blueprint belongs
	private final String contextName;
	
	// The properties of the world area created by the blueprint
	private final int worldAreaProperties;
	
	// An array containing all the world object blueprints for the world area
	private final WorldObjectBlueprint[] blueprints;
	
	/*============================================================
	Constructors
	============================================================*/
	
	/** Creates a new WorldAreaBlueprint which can be exported to a 
	 * 3dwa-file. The blueprint requires a context name in which the 
	 * world area blueprint was created, a set of properties for the 
	 * world area and a collection of SerializableWorldObjects associated
	 * with this world area blueprint. **/
	public WorldAreaBlueprint(	String contextName, int worldAreaProperties,
								Collection<DeflatableWorldObject> worldObjects) 
										throws WorldAreaBlueprintException {
		this.version = defaultVersion;
		this.contextName = contextName;
		this.worldAreaProperties = worldAreaProperties;
		blueprints = new WorldObjectBlueprint[worldObjects.size()];
		HashMap<DeflatableWorldObject,Integer> dependencyMap = 
				new HashMap<DeflatableWorldObject,Integer>();
		HashSet<DeflatableWorldObject> tracker = new HashSet<DeflatableWorldObject>();

		// Fill the blueprint array with blueprints of all serializable world objects
		int offset = 0;
		for (DeflatableWorldObject worldObject : worldObjects)
			offset += extractDependencies(dependencyMap,worldObject,blueprints,offset,tracker);
	}
	
	/** Creates a new WorldAreaBlueprint from a 3dwa-file. The blueprint
	 * requires a version and a context name in which the blueprint was
	 * created. The blueprint also needs the properties of the world area
	 * to create and a set of world object blueprints contained within this
	 * world area. **/
	@Friendly WorldAreaBlueprint(	int version, 
									String contextName, 
									int worldAreaProperties,
									WorldObjectBlueprint[] blueprints) {
		this.version = version;
		this.contextName = contextName;
		this.worldAreaProperties = worldAreaProperties;
		this.blueprints = blueprints;
	}
	
	/*============================================================
	Public Methods
	============================================================*/
	
	/** Returns the version of the world area blueprint loaded from a 3dwa-file. **/
	public int getVersion() {
		return version;
	}

	/** Returns the name of the context in which this world area blueprint belongs. **/
	public String getContextName() {
		return contextName;
	}
	
	/** Loads a WorldArea from the WorldAreaBlueprint by using a given
	 * group of world object inflators and a given collection used to
	 * catch non vital blueprint exceptions. An exception will be thrown
	 * to the non vital collection if the inflated instance throws a runtime
	 * exception during inflation (like BufferUnderflowException). **/
	public WorldArea load(	WorldObjectInflatorGroup inflatorGroup, 
							Collection<WorldAreaBlueprintException> nonVitalExceptions) 
			throws WorldAreaBlueprintException {
		WorldArea world = new WorldArea(worldAreaProperties);
		final WorldObject[] instances = new WorldObject[blueprints.length];
		
		for (int i = 0; i < blueprints.length; i++) {
			// Create each object from the blueprints using the given inflator group
			WorldObjectBlueprint blueprint = blueprints[i];
			String name = blueprint.getName();
			
			// Find the inflator
			WorldObjectInflator inflator = inflatorGroup.getInflator(name);
			if (inflator == null)
				nonVitalExceptions.add(new WorldAreaBlueprintException(	
													"No inflator was found which "+
													"supprots creation of instances"+
													" with the name \"" + 
													name + "\"."));
			else {
				
				// Retrieve dependencies
				final int[] dependencyIds = blueprint.getDependencies();
				WorldObject[] dependencies = new WorldObject[dependencyIds.length];
				for (int j = 0; j < dependencyIds.length; j++)
					dependencies[j] = instances[dependencyIds[j]];
			
				// Inflate the world object
				final WorldObjectDeflation deflation = 
						inflator.makeDeflation(blueprint.getDeflationProperties(),dependencies); 
				try {
					deflation.inflate(ByteBuffer.wrap(blueprint.getDeflationData()));
				} catch (RuntimeException e) {
					nonVitalExceptions.add(new WorldAreaBlueprintException(e));
				}
			
				
			
				// Add the inflated world object to the world
				final WorldObject instance = inflator.makeWorldObject(deflation);
				if (!deflation.skipCreation())
					world.createInstance(instance);
				instances[i] = instance;
			}
		}
		
		return world;
	}
	
	/*============================================================
	Friendly Methods
	============================================================*/
	
	/** Returns the properties of the world area created by the blueprint. **/
	public int getWorldAreaProperties() {
		return worldAreaProperties;
	}
	
	/** Returns a dependency sorted array of all the blueprints in this
	 * world area blueprint. **/
	public WorldObjectBlueprint[] getBlueprints() {
		return blueprints;
	}
	
	/*============================================================
	Private Methods
	============================================================*/
	
	/** Extracts all dependencies from a SerializableWorldObject and
	 * adds them to an array of SerializableWorldObjects at a given
	 * start index (offset). The extracted SerializableWorldObjects 
	 * will also be added into the provided hash map in order to keep 
	 * track already extracted SerializableWorldObjects. The returned
	 * value is the number of extracted dependencies. A hash set must
	 * also be provided (a tracker) which makes sure that no circular
	 * dependencies occur.**/
	private static final int extractDependencies(
							HashMap<DeflatableWorldObject,Integer> dependencyMap,
							DeflatableWorldObject worldObject, 
							WorldObjectBlueprint[] blueprints,
							int offset,
							HashSet<DeflatableWorldObject> tracker) 
		throws WorldAreaBlueprintException {
		// Check if the serializable world object is already extracted
		if (dependencyMap.containsKey(worldObject))
			return 0;
		
		if (tracker.contains(worldObject)) 
			throw new WorldAreaBlueprintException(	"Circular dependency was " + 
													"detected in deflatable " + 
													"world object with name " +
													"\"" + worldObject.getName() + 
													"\".");
		tracker.add(worldObject);
		
		// The number of extracted dependencies
		int extractedDependencies = 0;
		
		// Retrieve the deflation and the dependencies of the world object
		final WorldObjectDeflation deflation = worldObject.getDeflation();
		DeflatableWorldObject[] dependencies = 
				new DeflatableWorldObject[deflation.trueDependencies()];
		for (int i = 0; i < dependencies.length; i++)
			dependencies[i] = (DeflatableWorldObject) deflation.getTrueDependency(i);
		
		// Create the dependency array of the world object blueprint
		int[] dependencyIds = null;
		
		if (dependencies != null) {
			dependencyIds = new int[dependencies.length];
			
			for (int i = 0; i < dependencies.length; i++) {
				final DeflatableWorldObject dependency = dependencies[i];
				
				if (dependencyMap.containsKey(dependency))
					dependencyIds[i] = dependencyMap.get(dependency);
				else {
					extractedDependencies += extractDependencies(
													dependencyMap,
													dependency,
													blueprints,
													offset + extractedDependencies,
													tracker);
					// Retrieve the index of last created blueprint (the dependency id)
					dependencyIds[i] = offset + extractedDependencies - 1;
				}
			}
		}
		
		// Create an id the for the new blueprint
		final int id = offset + extractedDependencies;
		
		// Add a new blueprint to the blueprint array
		final byte[] data = deflation.deflate(0).array();
		blueprints[id] = 
				new WorldObjectBlueprint(	id,
											worldObject.getName(),
											dependencyIds,
											deflation.getProperties(),
											data);
		
		// Add the world object to the map
		dependencyMap.put(worldObject, id);
		return extractedDependencies + 1;
	}

}
